﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Speech.Recognition;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using System.Xml.Linq;

using Perseus;
using Perseus.Data;
using Perseus.Plugins;


using Hetymine.Executioner.Commands;
using Hetymine.Executioner.Data;
using Hetymine.Executioner.Plugins;
using Hetymine.Executioner.Triggers;

namespace Hetymine.Executioner {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class Executioner : Window {
        #region PInvoke
        /// <summary> The RegisterHotKey function defines a system-wide hot key </summary>
        /// <param name="hwnd">Handle to the window that will receive WM_HOTKEY messages 
        /// generated by the hot key.</param>
        /// <param name="id">Specifies the identifier of the hot key.</param>
        /// <param name="fsModifiers">Specifies keys that must be pressed in combination with the key 
        /// specified by the 'vk' parameter in order to generate the WM_HOTKEY message.</param>
        /// <param name="vk">Specifies the virtual-key code of the hot key</param>
        /// <returns><c>true</c> if the function succeeds; otherwise <c>false</c></returns>
        /// <seealso cref="http://msdn.microsoft.com/en-us/library/ms646309(VS.85).aspx"/>
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);

        [DllImport("user32", SetLastError = true)]
        private static extern int UnregisterHotKey(IntPtr hwnd, int id);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern ushort GlobalAddAtom(string lpString);

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        private static extern ushort GlobalDeleteAtom(ushort nAtom);

        private const int WM_HOTKEY = 0x312;
        #endregion
        #region Private Fields
        private static int _ExecutionCount; // To prevent self executing commands from getting stuck in an infinite loop
        private static int _MaxExecutionCount; // The max number of self executing loops before stopping.

        private static WindowInteropHelper _Handle; // Interop helper for hooking into messages
        private HwndSource _HwndSource;
        private System.Windows.Forms.NotifyIcon _NotifyIcon; // System tray icon (In case hotkeys are lost, need access)
        private System.Windows.Forms.ContextMenu _NotifyIconMenu;
        private DispatcherTimer _NotifyIconClickTimer;

        private bool _TaskbarMode; // Show executioner window in taskbar

        private bool _Loaded; // If executioner commands, config, history, etc. has been loaded

        // Commands and Hotkeys
        private HotKeyInfo _ShowCommandInput; // HotKey to show command input
        private List<HotKeyInfo> _HotKeys; // List of trigger hotkeys

        // History / Autocomplete
        private List<string> _History; // Array of command history        
        private int _HistoryIndex; // Current history item index to display (-1 = current, -2 blank)
        private List<string> _AutoComplete; // Array of possible autocomplete values
        private int _AutoCompleteIndex; // Current index of autocomplete list
        private bool _NoAutoCompleteUpdate; // When true, editing text field does not update autocomplete
        private string _CurrentCommand; // Currently typed command that hasnt been executed
        private bool _NoInputUpdate; // When true, input text changed event is ignored 

        private bool _IsError; // Has an error occured while executing a command
        
        // Timer to prevent user from holding down a key and executing 
        // a command in rapid succession
        private DispatcherTimer _HotKeyTimer;

        private Config _ConfigWindow;

        // Speech Objects
        private bool _SpeechInitialized;
        private SpeechRecognizer _SpeechRecognizer;
        #endregion
        #region Initialization
        public Executioner() {
            InitializeComponent();

            this.Loaded += new RoutedEventHandler(Executioner_Loaded);
            this.Deactivated += new EventHandler(Executioner_Deactivated);
            this.StateChanged += new EventHandler(Executioner_StateChanged);
            
            // Add key handlers for catching enter and command history
            this.cCommand.PreviewKeyDown += new KeyEventHandler(Command_PreviewKeyDown);
            this.cCommand.TextChanged += new TextChangedEventHandler(Command_TextChanged);
            this.cCommand.SelectionChanged += new RoutedEventHandler(Command_SelectionChanged);

            this._Loaded = false;
        }
        
        ~Executioner() {
            // This shouldn't be loaded if properly exited, if it is, some unloaders might fail do
            // to not having access to the windows handle
            if (this._Loaded) {
                this.SaveHistory();
                this.Unload();
            }
        }
        private void Executioner_Loaded(object sender, RoutedEventArgs e) {
            // Event to dynamically load assemblies used by plugins to allow them to
            // reside in the plugin directory instead of the main directory
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

            // Disable return so keydown will detect up and down keys
            this.cCommand.AcceptsReturn = false;        

            // Set command input and auto complete values to their defaults
            this.ResetCommandBox();
            this._NoInputUpdate = false;

            this.ResetCommandAutoComplete();
            this._NoAutoCompleteUpdate = false;

            // Quasi singletone implementation to allow commands to
            // execute other commands
            Executioner.Instance = this;
            Executioner._ExecutionCount = 0;
            Executioner._MaxExecutionCount = 5;

            // Timer to prevent rappid succession of hotkey commands
            this._HotKeyTimer = new DispatcherTimer();
            this._HotKeyTimer.Interval = TimeSpan.FromMilliseconds(400);
            this._HotKeyTimer.IsEnabled = false;
            this._HotKeyTimer.Tick += new EventHandler(HotKeyTimer_Tick);

            // Setup interop to hook into windows messages
            Executioner._Handle = new WindowInteropHelper(this);
            this._HwndSource = HwndSource.FromHwnd(Executioner._Handle.Handle);
            this._HwndSource.AddHook(new HwndSourceHook(this.HookHandler));

            Executioner.PluginService = new PluginService<Plugin>();

            // List of built in executioner commands
            this.ExecutionerCallStrings = new List<string>();
            this.ExecutionerCallStrings.AddRange(new string[] { 
                "-clear", "-cmds", "-config", "-exec", "-exit", "-reload", "-speech"
            });


            this._SpeechInitialized = false;

            // Load dynamic data such as configuration and commands
            this.Load();

            this.LoadHistory();

            // Execute command line
            List<string> args = new List<string>(Environment.GetCommandLineArgs());
            if (args.Count > 1) {
                args.RemoveAt(0);
                this.ExecuteCommand(string.Join(" ", args.ToArray()));
            }
        }

        private void Load() {
            try {
                // Load configuration from config.ini
                string config = AppDomain.CurrentDomain.BaseDirectory + "config.ini";
                if (File.Exists(config)) {
                    Executioner.Config = new IniFile(config);
                }
                else {
                    Executioner.Config = IniFile.FromResource(
                        new Uri("pack://application:,,,/Resources/config.ini")
                    );
                    Executioner.Config.FileName = config;
                }               
            }
            catch {
                App.Log("Error loading config.ini resource.");
                Executioner.Config = new IniFile();
            }

            Executioner._MaxExecutionCount = Executioner.Config.GetInt("Behaviour", "Max Execution Count", 5).MaxMin(1, 50);
            int interval = Math.Max(Executioner.Config.GetInt("Behaviour", "Hot Key Timeout", 400), 250);
            this._HotKeyTimer.Interval = TimeSpan.FromMilliseconds(interval);

            // Disable input method editor to false so it doesnt start off in non english
            // Temporary solution until localization is added if ever...            
            InputMethod.SetIsInputMethodEnabled(
                this.cCommand,
                Executioner.Config.GetBool("Behaviour", "Disable Input Method", false)
            );

            // Register command input hotkey
            string hotKey = Executioner.Config.GetString("Behaviour", "Command Box Hot Key", "Alt+C");
            this._ShowCommandInput = new HotKeyInfo(hotKey);
            this.RegisterHotkey(this._ShowCommandInput);

            // Check for plugins with custom load method
            foreach (PluginInstance<Plugin> p in Executioner.PluginService.Plugins) {                
                if (p.Instance is ILoader) {
                    try {
                        ((ILoader)p.Instance).Load();
                    }
                    catch {
                        // For whatever reason no error was produced when resource did not exists
                        App.Log("Error loading " + p.Name + " plugin.");
                    }
                }
            }

            // Load commands from commands.xml            
            this.LoadCommands();

            // Style command input
            Styles.TextBox(this.cCommand);
            Styles.InnerBorder(this.cInner);
            Styles.OuterBorder(this.cOuter);
            this.UpdateLayout();

            // Notification area
            if (Executioner.Config.GetBool("Behaviour", "Show Notification Icon", PEnvironment.IsWindowsVistaOrBelow)) {
                this.InitializeNotificationArea();
            }
            else {
                if (this._NotifyIcon != null) {
                    this._NotifyIcon.Dispose();
                }
            }

            // Taskbar
            if (Executioner.Config.GetBool("Behaviour", "Show In Taskbar", PEnvironment.IsWindows7OrAbove)) {
                // In the future add jumplist support
                this._TaskbarMode = true;
                this.ShowInTaskbar = true;

                this.Visibility = Visibility.Visible;
                this.WindowState = WindowState.Minimized;
            }
            else {
                this._TaskbarMode = false;
                this.ShowInTaskbar = false;

                this.Visibility = Visibility.Collapsed;
                this.WindowState = WindowState.Normal;
            }

            // Speech recognition
            if (Executioner.Config.GetBool("Behaviour", "Enable Speech") && this.TriggerTypeInUse(typeof(SpeechTrigger))) {
                this.EnableSpeech();
            }
            
            this._Loaded = true;
        }
        private void Unload() {
            this._Loaded = false;

            this.DisableSpeech();

            if (this._NotifyIcon != null) {
                this._NotifyIcon.Dispose();
            }

            // Check for plugins with custom unload method
            foreach (PluginInstance<Plugin> p in Executioner.PluginService.Plugins) {
                if (p.Instance is ILoader) {
                    try {
                        ((ILoader)p.Instance).Unload();
                    }
                    catch {
                        // For whatever reason no error was produced when resource did not exists
                        App.Log("Error unloading " + p.Name + " plugin.");
                    }
                }
            }

            // Unregister all hotkeys
            foreach (HotKeyInfo hk in this._HotKeys) {
                this.UnregisterHotkey(hk);
            }
            this.UnregisterHotkey(this._ShowCommandInput);

            Executioner.Config = null;
        }
        public void Reload() {
            this.Unload();
            this.Load();
        }

        private void InitializeNotificationArea() {
            // Setup tray icon and context menu
            // Uses System.Windows.Forms
            this._NotifyIcon = new System.Windows.Forms.NotifyIcon();
            try {
                this._NotifyIcon.Icon = new System.Drawing.Icon(
                    App.GetResourceStream(
                        new Uri("pack://application:,,,/Resources/executioner.ico")
                    ).Stream, 16, 16
                );
            }
            catch {
                App.Log("Error loading executioner.ico resource.");
            }
            this._NotifyIcon.Text = "Executioner";
            // Windows 7+ defaults to taskbar mode because of pinning and context menu options
            this._NotifyIcon.Visible = true;
            this._NotifyIcon.MouseClick += new System.Windows.Forms.MouseEventHandler(NotifyIcon_Click);
            this._NotifyIcon.DoubleClick += new EventHandler(NotifyIcon_DoubleClick);

            this._NotifyIconMenu = new System.Windows.Forms.ContextMenu();
            this._NotifyIconMenu.MenuItems.Add(new System.Windows.Forms.MenuItem("Reload Commands"));
            this._NotifyIconMenu.MenuItems[0].Click += new EventHandler(NotifyIconMenu_Reload_Click);
            this._NotifyIconMenu.MenuItems.Add(new System.Windows.Forms.MenuItem("Configuration"));
            this._NotifyIconMenu.MenuItems[1].Click += new EventHandler(NotifyIconMenu_Config_Click);
            this._NotifyIconMenu.MenuItems.Add("-");
            this._NotifyIconMenu.MenuItems.Add(new System.Windows.Forms.MenuItem("Toggle Speech [Disabled]"));
            this._NotifyIconMenu.MenuItems[3].Click += new EventHandler(NotifyIconMenu_ToggleSpeech_Click);
            this._NotifyIconMenu.MenuItems.Add("-");
            this._NotifyIconMenu.MenuItems.Add(new System.Windows.Forms.MenuItem("Website"));
            this._NotifyIconMenu.MenuItems[5].Click += new EventHandler(NotifyIconMenu_Website_Click);
            this._NotifyIconMenu.MenuItems.Add("-");
            this._NotifyIconMenu.MenuItems.Add(new System.Windows.Forms.MenuItem("Exit"));
            this._NotifyIconMenu.MenuItems[7].Click += new EventHandler(NotifyIconMenu_Exit_Click);

            this._NotifyIcon.ContextMenu = this._NotifyIconMenu;            

            this._NotifyIconClickTimer = new DispatcherTimer();
            this._NotifyIconClickTimer.Interval = TimeSpan.FromMilliseconds(100);
            this._NotifyIconClickTimer.IsEnabled = false;
            this._NotifyIconClickTimer.Tick += new EventHandler(NotifyIconClickTimer);
        }

        private void LoadCommands() {
            this._HotKeys = new List<HotKeyInfo>();
            this.Commands = new Dictionary<string, List<Hetymine.Executioner.Commands.ICommand>>();

            string fileName = AppDomain.CurrentDomain.BaseDirectory + "commands.xml";
            if (!File.Exists(fileName)) {
                return;
            }

            XDocument xmlDoc = XDocument.Load(fileName);

            if (xmlDoc.Element("Executioner") == null ||
                xmlDoc.Element("Executioner").Element("Commands") == null
            ) {
                return;
            }

            //Hetymine.Executioner.Commands.ICommand ;

            var commands = from cmd in xmlDoc.Element("Executioner").Element("Commands").Elements()
                           select cmd;

            foreach (var command in commands) {
                string type = command.Name.LocalName;
                switch (type) {
                    case "Trigger":
                        try {
                            TriggerCommand tc = new TriggerCommand();
                            tc.Load(command);
                            this.AddCommand("Trigger", tc);
                            var hotKeys = from hk in tc.Triggers
                                          where hk is HotKeyTrigger
                                          select hk as HotKeyInfo;
                            foreach (var hk in hotKeys) {
                                this.AddHotKey(hk);
                            }
                        }
                        catch {
                            App.Log("Error loading trigger command.");
                        }
                        break;
                    default:                        
                        try {
                            PluginInstance<Plugin> plugin = Executioner.PluginService[typeof(CommandPlugin), type];
                            if (plugin != null) {
                                CommandPlugin cp = plugin.Instance as CommandPlugin;
                                if (cp.CanInitialize) {
                                    Hetymine.Executioner.Commands.ICommand pc = cp.InitializeCommand();
                                    pc.Load(command);
                                    this.AddCommand(type, pc);
                                }
                            }
                            else {
                                UnknownCommand uc = new UnknownCommand();
                                uc.Load(command);
                                this.AddCommand("Unknown", uc);
                            }
                        }
                        catch {
                            App.Log("Error loading " + type +  " plugin command.");
                        }
                        break;
                }
            }
        }

        private void LoadHistory() {
            this._History = new List<string>();

            string file = AppDomain.CurrentDomain.BaseDirectory + "history.txt";
            if (File.Exists(file)) {
                string[] history = File.ReadAllLines(file);
                this._History.AddRange(history);
            }
        }
        private void SaveHistory() {
            string file = AppDomain.CurrentDomain.BaseDirectory + "history.txt";
            if (Executioner.Config.GetBool("Behaviour", "Save History", true)) {
                File.WriteAllLines(file, this._History.ToArray(), System.Text.Encoding.UTF8);
            }
            else if (File.Exists(file)) {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Dynamically resolves required assemblies used by plugins.
        /// </summary>
        /// <returns>The loaded assembly.</returns>
        private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) {
            string assemblyName = args.Name.Substring(0, args.Name.IndexOf(",")) + ".dll";

            string path = AppDomain.CurrentDomain.BaseDirectory + Path.DirectorySeparatorChar + "plugins";
            string[] dirs = Directory.GetDirectories(path);

            foreach (string d in dirs) {
                string[] files = Directory.GetFiles(d);
                foreach (string f in files) {
                    string fileName = Path.GetFileName(f);
                    if (fileName == assemblyName) {
                        return Assembly.LoadFrom(f);
                    }
                }
            }

            return null;
        }

        private void Exit() {
            this.SaveHistory();
            this.Unload();
            Application.Current.Shutdown();
        }
        #endregion
        #region Notification Icon Events
        private void NotifyIcon_Click(object sender, System.Windows.Forms.MouseEventArgs e) {
            if (e.Button == System.Windows.Forms.MouseButtons.Left) {                
                this._NotifyIconClickTimer.IsEnabled = true;                
            }
        }
        private void NotifyIcon_DoubleClick(object sender, EventArgs e) {
            this._NotifyIconClickTimer.IsEnabled = false;
            int clickBehaviour = Executioner.Config.GetInt("Behaviour", "Notification Icon Double Click", 1);
            this.NotifyIconClickBehaviour(clickBehaviour);
        }
        private void NotifyIconClickBehaviour(int clickBehaviour) {
            switch (clickBehaviour) {
                case 1:
                    this.ShowCommandBox();
                    break;
                case 2:
                    System.Reflection.MethodInfo mi = this._NotifyIcon.GetType().GetMethod(
                        "ShowContextMenu", 
                        BindingFlags.Instance | BindingFlags.NonPublic
                    );
                    mi.Invoke(this._NotifyIcon, null);
                    break;
                case 3:
                    this.ToggleSpeech();
                    break;
                case 4:
                    this.ShowConfig();
                    break;
            }
        }
        private void NotifyIconMenu_Reload_Click(object o, EventArgs e) {
            this.Reload();
        }
        private void NotifyIconMenu_Config_Click(object o, EventArgs e) {
            this.ShowConfig();   
        }
        private void NotifyIconMenu_ToggleSpeech_Click(object o, EventArgs e) {
            this.ToggleSpeech();
        }
        private void NotifyIconMenu_Website_Click(object o, EventArgs e) {
            Process.Start("http://hetymine.com/executioner/");
        }
        private void NotifyIconMenu_Exit_Click(object o, EventArgs e) {
            this.Exit();
        }
        private void NotifyIconClickTimer(object sender, EventArgs e) {
            this._NotifyIconClickTimer.IsEnabled = false;
            int clickBehaviour = Executioner.Config.GetInt("Behaviour", "Notification Icon Single Click", 2);
            this.NotifyIconClickBehaviour(clickBehaviour);
        }
        #endregion
        #region Command Box Handling
        private void ShowCommandBox() {
            this.ResetCommandBox();
            
            this.WindowState = WindowState.Normal;
            this.Visibility = Visibility.Visible;


            Point p = Styles.Position(this.ActualWidth, this.ActualHeight);

            this.Left = p.X;
            this.Top = p.Y;

            this.Activate();
            this.cCommand.Focus();
        }
        private void HideCommandBox() {
            if (this._TaskbarMode) {
                this.WindowState = WindowState.Minimized;
            }
            else {
                this.Visibility = Visibility.Collapsed;
            }
        }
        private void ResetCommandBox() {
            this.cCommand.Text = string.Empty;
            this._CurrentCommand = string.Empty;
            this._HistoryIndex = -1;
            this._IsError = false;
        }
        private void Command_PreviewKeyDown(object sender, KeyEventArgs e) {
            this._NoInputUpdate = true;
            switch (e.Key) {
                case Key.Enter: // Execute command!
                    string cmd = this.cCommand.Text;
                    if (Keyboard.Modifiers == (Keyboard.Modifiers | ModifierKeys.Shift)) {
                        // If there is a space and text selected, it is a history value 
                        // Override to make shift enter replace old value with new
                        int space = cmd.IndexOf(" ", StringComparison.Ordinal);
                        if (space > 0 && cCommand.SelectionLength > 0) {
                            cmd = cmd.Substring(0, space);
                        }
                        cmd += " " + Clipboard.GetText();
                    }
                    cmd = cmd.Trim();

                    if (!cmd.IsEmpty()) {
                        // Remove duplicate commands
                        while (this._History.Contains(cmd)) {
                            this._History.Remove(cmd);
                        }

                        int maxHistory = Executioner.Config.GetInt("Behaviour", "Max History Items", 250);
                        // Do not add exit or clear to histroy
                        if ((maxHistory == -1 || maxHistory > 0) &&
                            cmd != "-clear" &&
                            !cmd.StartsWith("-clear ", StringComparison.Ordinal) &&
                            cmd != "-exit" &&
                            cmd.IndexOf("\n") < 0 // If command has multiple lines, do not add to history
                        ) {
                            this._History.Insert(0, cmd);
                            if (maxHistory > 0 && this._History.Count > maxHistory) {
                                this._History.RemoveRange(maxHistory, this._History.Count - maxHistory);
                            }
                        }

                        if (!this.ExecuteCommand(cmd)) {
                            string defaultCommand = Executioner.Config.GetString("Behaviour", "Default Command");
                            if (!this._IsError && !defaultCommand.IsEmpty()) {
                                // If all else fails, run the default command
                                this.ExecuteCommand(defaultCommand + " " + cmd);
                            }
                        }
                    }

                    e.Handled = true;
                    this.HideCommandBox();
                    break;
                case Key.Down: // Next command
                    if (this._AutoComplete.Count > 1) {
                        this.AutoCompleteDown();
                    }
                    // If only one item left, set to current command and continue normally
                    else if (this._AutoComplete.Count == 1) {
                        this.ResetCommandAutoComplete();
                        this._HistoryIndex = -1;
                        this.cCommand.Text = this._CurrentCommand;
                        this.HistoryDown();
                    }
                    else {
                        this.HistoryDown();
                    }
                    e.Handled = true;
                    break;
                case Key.Up: // Previous command                
                    if (this._AutoComplete.Count > 1) {
                        this.AutoCompleteUp();
                    }
                    else if (this._AutoComplete.Count == 1) {
                        this.ResetCommandAutoComplete();
                        this._HistoryIndex = -1;
                        this.HistoryUp();
                    }
                    else {
                        this.HistoryUp();
                    }
                    e.Handled = true;
                    break;
                case Key.Escape: // Hide window on escape
                    this.HideCommandBox();
                    break;
                case Key.Delete:
                    this._NoAutoCompleteUpdate = true;
                    break;
                case Key.Back:
                    if (this.cCommand.SelectionStart > 0) {                        
                        this._NoAutoCompleteUpdate = true;
                        // If there is an auto complete value, then we need to
                        // selected one character more since only selection is
                        // deleted on backspace
                        if (this._AutoComplete.Count > 0) {
                            this.cCommand.SelectionStart = this.cCommand.SelectionStart - 1;
                            ++this.cCommand.SelectionLength;
                        }
                    }
                    break;
            }
            this._NoInputUpdate = false;
        }
        private void Command_TextChanged(object sender, TextChangedEventArgs e) {
            // If not in preview key down
            if (this._NoInputUpdate) {
                return;
            }

            this._HistoryIndex = -1;

            this.ResetCommandAutoComplete();

            if (this._NoAutoCompleteUpdate) {
                this._NoAutoCompleteUpdate = false;
                return;
            }

            if (!this.cCommand.Text.IsEmpty()) {
                // Get names of all string commands and history
                var names = from h in this._History
                            where h.StartsWith(this.cCommand.Text, StringComparison.Ordinal)
                            select h;

                var cmds = from c in this.CallStrings
                           where c.StartsWith(this.cCommand.Text, StringComparison.Ordinal)
                           select c;

                names = names.Union(cmds).OrderBy(p => p);

                foreach (string name in names) {
                    this._AutoComplete.Add(name);
                }

                // Enable auto complete selection if matching items exist
                if (this._AutoComplete.Count > 0) {
                    string s = this.cCommand.Text;
                    this._NoInputUpdate = true;
                    this.cCommand.Text = this._AutoComplete[0];
                    this.cCommand.Select(s.Length, this.cCommand.Text.Length - s.Length);
                    this._NoInputUpdate = false;
                }
            }
        }
        private void Command_SelectionChanged(object sender, RoutedEventArgs e) {
            if (!this._NoInputUpdate) {
                // If not selecting to end of text, clear auto complete
                if (this.cCommand.SelectionStart + this.cCommand.SelectionLength != cCommand.Text.Length) {
                    this.ResetCommandAutoComplete();
                }
                else {
                    foreach (string s in this._AutoComplete) {
                        if (s.Length < this.cCommand.SelectionStart) {
                            this.ResetCommandAutoComplete();
                            break;
                        }
                    }
                }
            }
        }
        private void HistoryUp() {
            if (this._HistoryIndex == -2) {
                this._HistoryIndex = -1;
                this.cCommand.Text = this._CurrentCommand;
            }
            else if (this._HistoryIndex < this._History.Count - 1) {
                if (this._HistoryIndex == -1) {
                    this._CurrentCommand = this.cCommand.Text;
                }
                ++this._HistoryIndex;
                this.cCommand.Text = this._History[this._HistoryIndex];
            }
            else {
                System.Media.SystemSounds.Beep.Play();
                return;
            }

            this.cCommand.SelectionStart = this.cCommand.Text.Length;
        }
        private void HistoryDown() {
            // There is a new command typed but not executed and has been cleared
            if (this._HistoryIndex == -2) {
                System.Media.SystemSounds.Beep.Play();
                return;
            }
            else if (this._HistoryIndex == -1) { // Current command index
                // If the current command is not blank, then to blank index (-2)
                if (!this.cCommand.Text.IsEmpty()) {
                    this._HistoryIndex = -2;
                    this._CurrentCommand = this.cCommand.Text;
                    this.cCommand.Text = string.Empty;
                }
                else {
                    System.Media.SystemSounds.Beep.Play();
                    return;
                }
            }
            else if (this._HistoryIndex == 0) { // Set to current command empty or not
                this._HistoryIndex = -1;
                this.cCommand.Text = this._CurrentCommand;
            }
            else {
                --this._HistoryIndex;
                this.cCommand.Text = this._History[this._HistoryIndex];
            }

            this.cCommand.SelectionStart = this.cCommand.Text.Length;
        }
        private void AutoCompleteUp() {
            if (this._AutoCompleteIndex == -1) {
                ++this._AutoCompleteIndex;
                int index = this._CurrentCommand.Length;
                this.cCommand.Text = this._AutoComplete[this._AutoCompleteIndex];
                this.cCommand.Select(index, this.cCommand.Text.Length - index);
            }
            else if (this._AutoCompleteIndex < this._AutoComplete.Count - 1) {
                ++this._AutoCompleteIndex;
                int index = this.cCommand.SelectionStart;
                this.cCommand.Text = this._AutoComplete[this._AutoCompleteIndex];
                this.cCommand.Select(index, this.cCommand.Text.Length - index);
            }
            else {
                System.Media.SystemSounds.Beep.Play();
            }
        }
        private void AutoCompleteDown() {
            if (this._AutoCompleteIndex == -1) {
                System.Media.SystemSounds.Beep.Play();
            }
            else if (this._AutoCompleteIndex == 0) {
                --this._AutoCompleteIndex;
                this._CurrentCommand = this.cCommand.Text.Substring(0, this.cCommand.SelectionStart);
                this.cCommand.Text = string.Empty;
            }
            else {
                --this._AutoCompleteIndex;
                int index = this.cCommand.SelectionStart;
                this.cCommand.Text = this._AutoComplete[this._AutoCompleteIndex];
                this.cCommand.Select(index, this.cCommand.Text.Length - index);
            }
        }
        private void ResetCommandAutoComplete() {
            this._AutoComplete = new List<string>();
            this._AutoCompleteIndex = 0;
        }
        private void Executioner_Deactivated(object sender, EventArgs e) {
            this.HideCommandBox();
        }
        private void Executioner_StateChanged(object sender, EventArgs e) {
            if (this._TaskbarMode && this.WindowState == WindowState.Normal) {
                this.ShowCommandBox();
            }
        }
        #endregion
        #region Command Execution
        private bool ExecuteCommand(string command) {
            command = command.Trim();
            
            string callString = string.Empty;
            string parameters = string.Empty;
            bool found = false;

            // Get all command names and reverse order for greedy 
            // command name in case name has spaces
            List<string> callStrings = this.CallStrings;
            callStrings.Add("-clear"); // Add back -clear command
            callStrings.OrderBy(p => p).Reverse();

            foreach (string cs in callStrings) {
                if (command == cs || command.StartsWith(cs + " ", StringComparison.Ordinal)) {
                    callString = command.Substring(0, cs.Length);
                    parameters = command.Substring(cs.Length).Trim();                    
                    found = true;
                    break;
                }
            }

            if (found) {
                if (this.ExecuteExecutionerCommand(callString, parameters) ||
                    this.ExecuteCallableCommand(callString, parameters)
                ) {
                    return true;
                }

                try {
                    // If the plugin is an executable type then execute it!
                    var plugins = from p in Executioner.PluginService.Plugins
                                  select p.Instance;
                    foreach (Plugin p in plugins) {
                        if (p is IExecutable) {
                            IExecutable e = p as IExecutable;
                            if (e.CallStrings.Contains(callString)) {
                                if (!e.Execute(parameters)) {
                                    this._IsError = true;
                                }
                                return true;
                            }
                        }
                        if (p is IExecutableCommands) {
                            IExecutableCommands e = p as IExecutableCommands;
                            if (e.CallStrings.Contains(callString)) {
                                if (!e.ExecuteCommand(callString, parameters)) {
                                    this._IsError = true;
                                }
                                return true;
                            }
                        }
                    }
                }
                catch {
                    Sounds.Error();
                    return true;
                }                
            }
            else {
                // Open url in browser
                if (this.ExecuteUrlCommand(command)) {
                    return true;
                }

                // Try to run it as a process
                if (Executioner.ExecuteProcessCommand(command)) {
                    return true;
                }
            }

            return false;
        }
        private bool ExecuteExecutionerCommand(string callString, string parameters) {
            if (callString == "-exit") {
                this.Exit();
                return true;
            }
            if (callString == "-reload") {
                this.Reload();
                return true;
            }
            // Open commands.xml
            if (callString == "-cmds") {
                Executioner.ExecuteProcessCommand(AppDomain.CurrentDomain.BaseDirectory + "\\commands.xml");
                return true;
            }
            if (callString == "-config") {
                this.ShowConfig();
                return true;
            }
            if (callString == "-exec") {
                Executioner.ExecuteProcessCommand(AppDomain.CurrentDomain.BaseDirectory);
                return true;
            }

            if (callString == "-speech") {
                if (parameters.IsEmpty()) {
                    this.ToggleSpeech();
                }
                else if (parameters == "on" || parameters == "1" || parameters == "true") {
                    this.EnableSpeech();
                    Executioner.Config.SetBool("Behaviour", "Enable Speech", false);
                }
                else if (parameters == "off" || parameters == "0" || parameters == "false") {
                    this.DisableSpeech();
                    Executioner.Config.SetBool("Behaviour", "Enable Speech", false);
                }
                else {
                    Sounds.ErrorNotify();
                }

                return true;
            }

            if (callString == "-clear") {
                if (parameters.IsEmpty()) {
                    this._History = new List<string>();
                }
                else {
                    List<string> newHistory = new List<string>();
                    foreach (string h in this._History) {
                        if (h == parameters || h.StartsWith(parameters + " ", StringComparison.Ordinal)) {
                            continue;
                        }
                        newHistory.Add(h);
                    }
                    this._History = newHistory;
                }
                return true;
            }

            return false;
        }

        private bool ExecuteCallableCommand(string callString) {
            return this.ExecuteCallableCommand(callString, string.Empty);
        }
        private bool ExecuteCallableCommand(string callString, string parameters) {
            if (this.Commands.ContainsKey("Trigger") && parameters.IsEmpty()) {
                var triggers = from tc in this.Commands["Trigger"]
                               where tc.CallStrings.Contains(callString)
                               select tc;

                // If no parameters and call string matches a trigger command
                if (triggers.Count() > 0) {
                    foreach (TriggerCommand tc in triggers) {                        
                        if (!tc.Execute(string.Empty)) {
                            this._IsError = true;
                        }
                    }

                    return true;
                }
            }

            bool success = false; // Has a command been executed
            
            // Otherwise execute all other commands            
            var commands = from cg in this.Commands.Keys
                            from c in this.Commands[cg]
                            where cg != "Trigger" && c.CallStrings.Contains(callString)
                            select c;

            if (commands.Count() > 0) {
                foreach (var c in commands) {                    
                    if (!c.Execute(parameters)) {
                        this._IsError = true;
                    }
                }

                success = true;
            }

            return success;
        }

        private bool ExecuteUrlCommand(string command) {            
            int index = command.IndexOf("://", StringComparison.Ordinal); // Any protocal
            if (index != -1) {
                if (index < 1) { return false; } // Shouldn't start as ://

                return Executioner.ExecuteProcessCommand(command);
            }

            // Local web server
            if (command.StartsWith("localhost", StringComparison.Ordinal)) {
                return Executioner.ExecuteProcessCommand("http://" + command);
            }

            // www is most likely a url
            if (command.StartsWith("www.", StringComparison.Ordinal)) {
                return Executioner.ExecuteProcessCommand("http://" + command);
            }

            // Assume commands ending in .exe are not urls and rather the user
            // relying on the default command to for example search for information
            // on that exe file
            if (command.EndsWith(".exe", StringComparison.Ordinal)) {
                return false;
            }

            // If a space is found, assume not a url
            index = command.IndexOf(" ", StringComparison.Ordinal);
            if (index > 0) {
                return false;
            }

            // Urls should have at least 1 period in it
            index = command.IndexOf(".", StringComparison.Ordinal);
            if (index > 1) {
                // Add protocal if non existant
                if (command.IndexOf("://", StringComparison.Ordinal) > 1) {
                    return Executioner.ExecuteProcessCommand(command);
                }
                else {
                    return Executioner.ExecuteProcessCommand("http://" + command);
                }
            }         

            return false;
        }
        public static bool ExecuteProcessCommand(string command) {
            return Executioner.ExecuteProcessCommand(command, string.Empty);
        }
        public static bool ExecuteProcessCommand(string command, string arguments) {
            try {
                if (command.StartsWith("@execute:")) {
                    ++Executioner._ExecutionCount;

                    // We set a max recursive execute amount to prevent possability of infinite loop
                    if (Executioner._ExecutionCount > Executioner._MaxExecutionCount) {
                        --Executioner._ExecutionCount;
                        return false;
                    }

                    command = command.Substring(9);
                    Executioner.Instance.ExecuteCommand(command + " " + arguments);
                    --Executioner._ExecutionCount;
                }
                else if (!arguments.IsEmpty()) {
                    if (arguments.StartsWith("@display:")) {
                        Display d = new Display();
                        d.Text = File.ReadAllText(command, System.Text.Encoding.Default);

                        int t;
                        if (int.TryParse(arguments.SafeSubstring(9), out t)) {
                            t = t.MaxMin(500, 10000);
                            d.Time = t;
                        }

                        d.Show();
                    }
                    else {
                        try { // Localized try catch to play back notify error when command doesn't exist
                            Process.Start(command, arguments);
                        }
                        catch {
                            Sounds.ErrorNotify();
                        }
                    }
                }
                else {
                    // Localized try catch to play back notify error when command doesn't exist
                    try {
                        Process.Start(command);
                    }
                    catch {
                        Sounds.ErrorNotify();
                    }
                }

                return true;
            }
            catch {
                Sounds.Error();                
            }

            return false;
        }       
        #endregion
        #region Config
        private void ShowConfig() {
            if (this._ConfigWindow != null) {
                this._ConfigWindow.Activate();
                return;
            }
            this._ConfigWindow = new Config();
            this._ConfigWindow.Closed += new EventHandler(Config_Closed);
            this._ConfigWindow.Show();
            if (this._NotifyIcon != null) {
                this._NotifyIconMenu.MenuItems[1].Enabled = false;
            }
        }
        private void Config_Closed(object sender, EventArgs e) {
            if (this._NotifyIcon != null) {
                this._NotifyIconMenu.MenuItems[1].Enabled = true;
            }
            this._ConfigWindow = null;
            this.Reload();
        }
        #endregion
        #region Speech
        private void ToggleSpeech() {
            if (this._SpeechInitialized == true) {
                this.DisableSpeech();
                Executioner.Config.SetBool("Behaviour", "Enable Speech", false);
            }
            else {
                this.EnableSpeech();
                Executioner.Config.SetBool("Behaviour", "Enable Speech", true);
            }
        }
        private void EnableSpeech() {
            try {
                if (!this._SpeechInitialized) {
                    if (this.Commands.ContainsKey("Trigger")) {
                        var speechTriggers = from c in this.Commands["Trigger"]
                                             from t in ((TriggerCommand)c).Triggers
                                             where t is SpeechTrigger
                                             select t;
                        // Only initialize the recognizer if there are actual speech phrases
                        if (speechTriggers.Count() > 0) {
                            this._SpeechRecognizer = new SpeechRecognizer();
                            this._SpeechRecognizer.SpeechRecognized += new EventHandler<SpeechRecognizedEventArgs>(_SpeechRecognizer_SpeechRecognized);
                            this._SpeechRecognizer.StateChanged += new EventHandler<StateChangedEventArgs>(_SpeechRecognizer_StateChanged);
                            GrammarBuilder grammar = new GrammarBuilder();
                            Choices phrases = new Choices();

                            foreach (SpeechTrigger t in speechTriggers) {
                                phrases.Add(t.Phrase);
                            }

                            grammar.Append(phrases);
                            this._SpeechRecognizer.LoadGrammar(new Grammar(grammar));

                            this._SpeechInitialized = true;
                        }
                    }
                }

                if (this._NotifyIcon != null) {
                    if (this._SpeechRecognizer != null) {
                        if (this._SpeechRecognizer.State == RecognizerState.Listening) {
                            this._NotifyIconMenu.MenuItems[3].Text = "Toggle Speech [Listening]";
                        }
                        else {
                            this._NotifyIconMenu.MenuItems[3].Text = "Toggle Speech [Not Listening]";
                        }
                    }
                    else {
                        this._NotifyIconMenu.MenuItems[3].Text = "Toggle Speech [No Phrases]";
                    }

                    this._NotifyIconMenu.MenuItems[3].Checked = true;
                }
            }
            catch {
                if (this._NotifyIcon != null) {
                    this._NotifyIconMenu.MenuItems[3].Visible = false;
                }
            }
        }        
        private void DisableSpeech() {
            if (this._SpeechRecognizer != null) {
                this._SpeechRecognizer.Dispose();
            }

            this._SpeechInitialized = false;
            
            if (this._NotifyIcon != null) {
                this._NotifyIconMenu.MenuItems[3].Checked = false;
                this._NotifyIconMenu.MenuItems[3].Text = "Toggle Speech [Disabled]";
            }
        }
        private void _SpeechRecognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e) {
            string phrase = e.Result.Text;

            foreach (TriggerCommand tc in this.Commands["Trigger"]) {
                foreach (ITrigger t in tc.Triggers) {
                    if (t is SpeechTrigger) {
                        if (((SpeechTrigger)t).Phrase == phrase) {
                            tc.Execute(string.Empty);
                            break;
                        }
                    }
                }
            }
        }
        private void _SpeechRecognizer_StateChanged(object sender, StateChangedEventArgs e) {
            if (this._NotifyIcon != null) {
                if (e.RecognizerState == RecognizerState.Stopped) {
                    this._NotifyIconMenu.MenuItems[3].Text = "Toggle Speech [Not Listening]";
                }
                else {
                    this._NotifyIconMenu.MenuItems[3].Text = "Toggle Speech [Listening]";
                }
            }
        }
        #endregion
        #region Register and Unregister Hotkeys
        private void AddHotKey(HotKeyInfo hotKey) {
            var check = from hk in this._HotKeys
                        where hk.Key == hotKey.Key && hk.Modifiers == hotKey.Modifiers
                        select hk;

            // Register the hotkey if no matching hotkey has been registered yet
            if (check.Count() == 0) {
                this._HotKeys.Add(hotKey);

                if (hotKey.Key == this._ShowCommandInput.Key && hotKey.Modifiers == this._ShowCommandInput.Modifiers) {
                    // Show command input hotkey already registered so use same id
                    hotKey.Id = this._ShowCommandInput.Id;
                }
                else {                    
                    this.RegisterHotkey(hotKey);
                }
            }
        }
        private const int ERROR_HOTKEY_ALREADY_REGISTERED = 1409;
        private void RegisterHotkey(HotKeyInfo hotKey) {
            try {
                string atom = this.Dispatcher.Thread.ManagedThreadId.ToString("X8") +
                    KeyInterop.VirtualKeyFromKey(hotKey.Key);
                hotKey.Id = GlobalAddAtom(atom);
                if (hotKey.Id == 0) { throw new Exception(); }
                hotKey.Registered = true;

                if (!RegisterHotKey(
                    Executioner._Handle.Handle,
                    hotKey.Id,
                    (uint)hotKey.Modifiers,
                    (uint)KeyInterop.VirtualKeyFromKey(hotKey.Key)
                )) { throw new Exception(); }
            }
            catch {
                if (Marshal.GetLastWin32Error() == ERROR_HOTKEY_ALREADY_REGISTERED) {
                    App.Log("The hotkey is already registered.");
                }
                else {
                    App.Log("Error registereding hotkey.");
                }                

                UnregisterHotkey(hotKey);
                hotKey.Registered = false;
            }
        }
        private delegate void ThreadStart();
        private void UnregisterHotkey(HotKeyInfo hotKey) {
            if (hotKey != null && hotKey.Id != 0 && hotKey.Registered == true) {
                DispatcherOperation dop = this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate() {
                    UnregisterHotKey(Executioner._Handle.Handle, hotKey.Id);
                    GlobalDeleteAtom(hotKey.Id);
                });

                // In case it's a reloading, wait for all to be unregistered as to
                // prevent registering an existing hotkey
                dop.Wait();
            }
        }        
        #endregion
        #region Window Hook Handler
        private IntPtr HookHandler(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) {
            handled = false;

            if (!this.IsLoaded) {
                return IntPtr.Zero;
            }

            if (msg != WM_HOTKEY) {
                var plugins = from p in Executioner.PluginService.Plugins
                              where p.Instance is IWindowHook
                              select p.Instance;
                foreach (IWindowHook p in plugins) {
                    // Later in config have executioner order and stops when handler returns true
                    // to resolve conflicts in plugin message handling
                    bool stop = p.HookHandler(hwnd, msg, wParam, lParam, ref handled);
                    if (handled || stop) {
                        return IntPtr.Zero;
                    }                    
                }

                return IntPtr.Zero;
            }

            if (this._HotKeyTimer.IsEnabled == true) {
                return IntPtr.Zero;
            }

            ushort id = (ushort)wParam.ToInt32();
            this._HotKeyTimer.IsEnabled = true;

            var hotKeys = from hk in this._HotKeys
                          where hk.Id == id
                          select hk;

            // If a matching registered hotkey is found, execute all 
            // trigger commands with a matching hotkey trigger
            if (hotKeys.Count() > 0) {
                HotKeyInfo hk = hotKeys.First();
                var triggers = from tc in this.Commands["Trigger"]
                               from hkt in ((TriggerCommand)tc).Triggers
                               where hkt is HotKeyTrigger && 
                               ((HotKeyTrigger)hkt).Key == hk.Key && 
                               ((HotKeyTrigger)hkt).Modifiers == hk.Modifiers
                               select tc as TriggerCommand;

                foreach (TriggerCommand tc in triggers) {
                    tc.Execute(string.Empty);
                }
            }

            // If hotkey matches the show command input hotkey
            if (id == this._ShowCommandInput.Id) {
                this.ShowCommandBox();
            }
            
            handled = true;

            return IntPtr.Zero;
        }
        private void HotKeyTimer_Tick(object sender, EventArgs e) {
            this._HotKeyTimer.IsEnabled = false;
        }
        public static IntPtr Handle {
            get { return Executioner._Handle.Handle; }
        }        
        #endregion
        #region Properties
        public static IniFile Config { get; set; }
        public static Executioner Instance { get; set; }
        public static PluginService<Plugin> PluginService { get; protected set; }
        public Dictionary<string, List<Hetymine.Executioner.Commands.ICommand>> Commands { get; protected set; }
        private List<string> ExecutionerCallStrings { get; set; }
        private List<string> CallStrings {
            get {
                List<string> execStrings = new List<string>();

                var commands = from cg in this.Commands.Values
                               from c in cg
                               where !(c is UnknownCommand)
                               select c;

                foreach (IExecutable c in commands) {
                    if (c.CallStrings != null) {
                        execStrings.AddRange(c.CallStrings);
                    }
                }

                var plugins = from p in Executioner.PluginService.Plugins
                              select p.Instance;
                foreach (Plugin p in plugins) {
                    if (p is IExecutable) {
                        IExecutable e = p as IExecutable;
                        if (e.CallStrings != null) {
                            execStrings.AddRange(e.CallStrings);
                        }
                    }

                    if (p is IExecutableCommands) {
                        IExecutableCommands e = p as IExecutableCommands;
                        if (e.CallStrings != null) {
                            execStrings.AddRange(e.CallStrings);
                        }
                    }
                }

                execStrings.AddRange(this.ExecutionerCallStrings);
                // remove -clear to prevent accidently clearing all your history
                execStrings.Remove("-clear");

                return execStrings;
            }
        }
        #endregion
        #region Miscellaneous
        private bool TriggerTypeInUse(Type triggerType) {
            var triggers = from tc in this.Commands["Trigger"]
                           from hkt in ((TriggerCommand)tc).Triggers
                           where hkt.GetType().Equals(triggerType)
                           select tc as TriggerCommand;

            if (triggers.Count() > 0) {
                return true;
            }

            return false;
        }
        private void AddCommand(string type, Hetymine.Executioner.Commands.ICommand command) {
            if (command == null) { return; }

            if (!this.Commands.ContainsKey(type)) {
                this.Commands.Add(type, new List<Hetymine.Executioner.Commands.ICommand>());
            }
            this.Commands[type].Add(command);
        }        
        #endregion
    }
}
